<?php
/**
 * S3 Client
 * @package dd_aws
 */

require_once('dd_aws_s3_RestRequest.php');

/**
 * S3 Client
 * @package dd_aws
 */
class dd_aws_s3_Client {

    /**
     * Private
     */
    const ACL_PRIVATE = 'private';

    /**
     * Public Read
     */
    const ACL_PUBLIC_READ = 'public-read';

    /**
     * Public Read and Write
     */
    const ACL_PUBLIC_READ_WRITE = 'public-read-write';

    /**
     * Access Key ID
     * @var string
     */
    private $accessKey;

    /**
     * Secret Access Key
     * @var string
     */
    private $secretAccessKey;

    /**
     * Use SSL?
     * @var bool
     */
    private $useSsl;

    /**
     * List of regex exclusions
     */
    private $exclusions;

    /**
     * Constructor
     * @param string $accessKey Access Key ID
     * @param string $secretAccessKey Secret Access Key
     * @param bool $useSsl Use SSL
     */
    public function __construct($accessKey, $secretAccessKey, $useSsl = false) {
        $this->accessKey = $accessKey;
        $this->secretAccessKey = $secretAccessKey;
        $this->useSsl = $useSsl;
        $this->exclusions = array();
    }

    /**
     * Add an exclusion
     */
    public function addExclusion($exclusion) {
        $this->exclusions[] = $exclusion;
    }

    /**
     * Add exclusions
     */
    public function addExclusions($exclusions) {
        $this->exclusions = array_merge($this->exclusions, $exclusions);
    }

    /**
     * Reset exclusions
     */
    public function resetExclusions() {
        $this->exclusions = array();
    }

    /**
     * Add default exclusions
     */
    public function addDefaultExclusions() {
        $this->addS3foxExclusions();
        return $this;
    }

    /**
     * Add s3fox exclusions
     *
     * Ignore _$folder$ entries created by s3fox.
     */
    public function addS3foxExclusions() {
        $this->addExclusions(array('_\$folder\$$'));
    }

    /**
     * Filter exclusions from results
     */
    protected function filter($results) {
        if ( ! empty($this->exclusions) ) {
            $regex = '/(' . implode('|', $this->exclusions) . ')/';
            foreach ( $results as $resultIdx => $result ) {
                if ( preg_match($regex, $result['key']) ) {
                    unset($results[$resultIdx]);
                }
            }
        }
        return $results;
    }

    /**
     * List buckets
     * @return array Bucket structure
     */
    public function listBuckets() {

        $request = $this->makeRequest('GET', '/');
        $response = $request->execute();

        list($dom, $xpath) = $this->makeDomXpath($response['body']);

        $buckets = array();

        foreach ( $xpath->query('./a:Buckets/a:Bucket') as $bucketElement ) {
            $buckets[] = array(
                'name' => $xpath->evaluate(
                    'string(./a:Name/text())',
                    $bucketElement
                ),
                'creationDate' => $xpath->evaluate(
                    'string(./a:CreationDate/text())',
                    $bucketElement
                ),
            );
        }

        return array(
            'owner' => array(
                'id' => $xpath->evaluate('string(./a:Owner/a:ID/text())'),
                'displayName' => $xpath->evaluate(
                    'string(./a:Owner/a:DisplayName/text())'
                ),
            ),
            'buckets' => $buckets,
        );

    }

    /**
     * Create a bucket
     * @param string $name Bucket name
     * @param string $acl ACL
     */
    public function createBucket($name, $acl = self::ACL_PRIVATE) {
        $amzHeaders = array('x-amz-acl' => $acl);
        $request = $this->makeRequest(
            'PUT',
            '/' . $name . '/',
            null, // query params
            null, // headers
            $amzHeaders
        );
        $response = $request->execute();
        if ( $response['code'] == 200 ) return true;
        return false;
    }

    public function deleteBucket($name) {
        $request = $this->makeRequest('DELETE', '/' . $name . '/');
        $response = $request->execute();
        if ( $response['code'] == 204 ) return true;
        return false;
    }

    /**
     * Convenience method to get the entire contents of a bucket
     */
    public function getBucketContents($name, $prefix = null, $delimiter = null) {

        $doContinue = true;

        $nextMarker = null;

        $contents = array();
        $commonPrefixes = array();

        while ( $doContinue ) {

            // This shoudl work recursively.
            $response = $this->getBucket($name, $prefix, $delimiter, null, $nextMarker);

            // We want to update our contents.
            $contents = array_merge($contents, $response['contents']);
            $commonPrefixes = array_merge($commonPrefixes, $response['commonPrefixes']);

            if ( strlen($response['nextMarker']) == 0 ) {
                // If no next marker is specified, we are probably
                // done looping.
                $doContinue = false;
            } else {
                $nextMarker = $response['nextMarker'];
            }

        }
        return array(
            'contents' => $contents,
            'commonPrefixes' => $commonPrefixes
        );
    }

    /**
     * Get a bucket
     */
    public function getBucket($name, $prefix = null, $delimiter = null, $maxKeys = null, $marker = null) {

        $queryParams = array();

        if ( ! is_null($prefix) ) $queryParams['prefix'] = $prefix;
        if ( ! is_null($delimiter) ) $queryParams['delimiter'] = $delimiter;
        if ( ! is_null($marker) ) $queryParams['marker'] = $marker;
        if ( ! is_null($maxKeys) ) $queryParams['max-keys'] = $maxKeys;

        $request = $this->makeRequest('GET', '/' . $name . '/', $queryParams);
        $response = $request->execute();

        list($dom, $xpath) = $this->makeDomXpath($response['body']);

        $contents = array();
        foreach ( $xpath->query('./a:Contents') as $contentElement ) {

            $contents[] = array(
                'key' => $xpath->evaluate('string(./a:Key/text())', $contentElement),
                'lastModified' => $xpath->evaluate('string(./a:LastModified/text())', $contentElement),
                'eTag' => $xpath->evaluate('string(./a:ETag/text())', $contentElement),
                'size' => $xpath->evaluate('string(./a:Size/text())', $contentElement),
                'storageClass' => $xpath->evaluate('string(./a:StorageClass/text())', $contentElement),
                'owner' => array(
                    'id' => $xpath->evaluate('string(./a:Owner/a:ID/text())', $contentElement),
                    'displayName' => $xpath->evaluate('string(./a:Owner/a:DisplayName/text())', $contentElement),
                ),
            );
        }

        $commonPrefixes = array();
        foreach ( $xpath->query('./a:CommonPrefixes/a:Prefix/text()') as $prefixElement ) {
            $commonPrefixes[] = $prefixElement->textContent;
        }

        // Does the XML say we are truncated?
        $isTruncated = $xpath->evaluate('string(./a:IsTruncated/text())') == 'true' ? true : false;

        // It is possible that we will have a next marker specified for
        // us already. If so, great! Use that.
        $nextMarker = $xpath->evaluate('string(./a:NextMarker/text())');
        if ( $isTruncated and ! $nextMarker ) {
            if ( count($contents) > 0 ) {
                $nextMarker = $contents[count($contents)-1]['key'];
            }
        }

        return array(
            'name' => $xpath->evaluate('string(./a:Name/text())'),
            'prefix' => $xpath->evaluate('string(./a:Prefix/text())'),
            'marker' => $xpath->evaluate('string(./a:Marker/text())'),
            'maxKeys' => $xpath->evaluate('string(./a:MaxKeys/text())'),
            'isTruncated' => $isTruncated,
            'delimiter' => $xpath->evaluate('string(./a:Delimiter/text())'),
            'contents' => $this->filter($contents),
            'commonPrefixes' => $commonPrefixes,
            'nextMarker' => $nextMarker,
        );

    }

    /**
     * Put object
     * @param string $bucketName Bucket name
     * @param string $objectName Object name
     * @param mixed $input Input
     * @param string $acl ACL
     * @param array $headers Associative array of headers
     * @param array $amzHeaders Associative array of Amazon headers
     */
    public function putObject($bucketName, $objectName, $input = null, $acl = self::ACL_PRIVATE, $headers = array(), $amzHeaders = array()) {
        $amzHeaders = array('x-amz-acl' => $acl);
        $request = $this->makeRequest(
            'PUT',
            '/' . $bucketName . '/' . $objectName,
            null, // query params
            $headers,
            $amzHeaders
        );
        $request->setInfile($input);
        $response = $request->execute();
        if ( $response['code'] == 200 ) return true;
        return false;
    }

    /**
     * Delete an object
     * @param string $bucketName Bucket name
     * @param string $objectName Object name
     */
    public function deleteObject($bucketName, $objectName) {
        $request = $this->makeRequest(
            'DELETE',
            '/' . $bucketName . '/' . $objectName
        );
        $response = $request->execute();
        if ( $response['code'] == 200 ) return true;
        return false;
    }

    /**
     * Make request
     * @param string $verb Verb
     * @param string $resource Resource
     * @param array $queryParams Query params
     * @param array $headers Associative array of headers
     * @param array $amzHeaders Associative array of Amazon headers
     * @param array $curlOpts Associative array of curl options
     * @param bool $useSsl Use SSL?
     * @return dd_aws_s3_RestRequest REST request object
     */
    protected function makeRequest($verb, $resource, $queryParams = null, $headers = null, $amzHeaders = null, $curlOpts = null, $useSsl = null) {
        if ( $useSsl === null ) $useSsl = $this->useSsl;
        if ( is_null($queryParams) ) $queryParams = array();
        $queryString = '';
        if ( count($queryParams) ) {
            $q = array();
            foreach ( $queryParams as $key => $value ) {
                $q[] = $key . '=' . rawurlencode($value);
            }
            if ( count($q) ) {
                $queryString = '?' . implode('&', $q);
            }
        }
        return new dd_aws_s3_RestRequest(
            $this,
            $this->accessKey,
            $this->secretAccessKey,
            $verb,
            's3.amazonaws.com',
            $resource . $queryString,
            $headers,
            $amzHeaders,
            $curlOpts,
            $useSsl
        );
    }

    /**
     * Make a DOM and XPath object from XML
     * @return array ($dom, $xpath)
     */
    protected function makeDomXpath($xml) {
        $dom = new DOMDocument();
        $dom->loadXML($xml);
        return array($dom, $this->makeXpath($dom));
    }

    /**
     * Make AWS S3 XPath for a DOM object
     * @return object dom
     */
    protected function makeXpath($dom) {
        $xpath = new DOMXPath($dom);
        $xpath->registerNamespace('a', 'http://s3.amazonaws.com/doc/2006-03-01/');
        return $xpath;
    }

    /**
     * Private
     */
    public function aclPrivate() { return self::ACL_PRIVATE; }

    /**
     * Public Read
     */
    public function aclPublicRead() { return self::ACL_PUBLIC_READ; }

    /**
     * Public Read and Write
     */
    public function aclPublicReadWrite() { return self::ACL_PUBLIC_READ_WRITE; }


}

?>
